设计模式-面向抽象编程(IOC+里氏代换原则)
设计模式-面向抽象编程(IOC+里氏代换原则)
B1n_依赖倒转原则详解
定义:
依赖倒转原则(Dependency Inversion Principle,简称 DIP)是面向对象设计(OOP)中的一项重要原则,其核心思想是:
高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
理解:
- 高层模块: 通常是指业务逻辑层或应用层,负责处理业务逻辑。(Service层)
- 低层模块: 通常是指数据访问层或基础设施层,负责提供数据访问或基础设施服务。(DAO层)
- 抽象: 指的是接口或抽象类,定义了模块之间的依赖关系。
- 细节: 指的是具体的实现类,实现了抽象所定义的功能。
依赖倒转的实现:
依赖倒转原则可以通过使用 接口 或 抽象类 来实现。
- 高层模块和低层模块都依赖于抽象,而不是具体的实现类。
- 抽象定义了模块之间的依赖关系,具体实现类实现了抽象所定义的功能。
依赖倒转的优点:
- 提高代码的灵活性和可维护性: 依赖倒转使得代码更加模块化,易于修改和扩展。
- 降低耦合度: 依赖倒转使得高层模块和低层模块之间耦合度降低,提高了代码的可重用性。
- 提高代码的测试性: 依赖倒转使得代码更容易测试,因为可以很容易地模拟抽象。
依赖倒转的应用:
依赖倒转原则可以应用于各种软件设计场景,例如:
- 框架设计: 许多框架都采用了依赖倒转原则,例如 Spring 框架。
- 业务逻辑设计: 可以使用依赖倒转原则来解耦业务逻辑和数据访问层。
- 测试驱动开发: 依赖倒转原则可以使测试驱动开发更加容易。
示例:
假设有一个应用程序,需要访问数据库。我们可以使用依赖倒转原则来设计该应用程序:
- 定义一个 数据库访问接口,定义了数据库访问操作。
- 创建一个 数据库访问实现类,实现了 数据库访问接口。
- 业务逻辑层依赖于 数据库访问接口,而不是具体的 数据库访问实现类。
这样,我们可以很容易地将业务逻辑层与数据库访问层解耦,提高代码的灵活性和可维护性。
总结:
依赖倒转原则是一项重要的面向对象设计原则,可以提高代码的灵活性和可维护性,降低耦合度,提高代码的测试性。
只是这样还不太能看出来依赖倒转究竟有什么好处,我们用代码来展示。
依赖倒转原则示例
场景:
假设我们有一个简单的应用程序,该应用程序需要将数据存储在数据库中。
传统方法:
传统的做法是直接在业务逻辑层中依赖数据库访问层。例如:
1 | public class BusinessLogic { |
这种方法耦合度很高,如果需要更改数据库访问层,则需要同时修改业务逻辑层。
依赖倒转原则:
我们可以使用依赖倒转原则来解耦业务逻辑层和数据库访问层。
定义一个
数据库访问接口
,定义了数据库访问操作:1
2
3
4
5public interface DatabaseAccess {
void saveData(String data);
}创建一个
数据库访问实现类
,实现了数据库访问接口
:1
2
3
4
5
6
7
8public class DatabaseAccessImpl implements DatabaseAccess {
public void saveData(String data) {
// 具体的数据库操作
}
}业务逻辑层依赖于
数据库访问接口
,而不是具体的数据库访问实现类
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class BusinessLogic {
private DatabaseAccess databaseAccess;
public BusinessLogic(DatabaseAccess databaseAccess) {
this.databaseAccess = databaseAccess;
}
public void saveData(String data) {
databaseAccess.saveData(data);
}
public String getData() {
return databaseAccess.getData();
}
}1
2
3
4
5
6
7DatabaseAccess application = new DatabaseAccess(new MySQLAccess());
String data = databaseAccess.getData();
System.out.println(data); // 输出 "data from MySQL"
DatabaseAccess databaseAccess = new Application(new OracleAccess());
String data = databaseAccess.getData();
System.out.println(data); // 输出 "data from Oracle"这样,业务逻辑层与数据库访问层就解耦了,我们可以很容易地将
数据库访问实现类
替换成其他实现,例如使用不同的数据库,如果您需要将数据存储方式从 MySQL 改为 Oracle,只需要将 BusinessLogic 类的构造函数参数改为 OracleDataAccessImpl 即可,而不需要修改业务逻辑层代码,这就是高层模块不应该依赖于低层模块,二者都应该依赖于抽象(二者都依赖于DatabaseAccess接口)
依赖注入:
我们可以使用依赖注入框架来将数据库访问接口
注入到业务逻辑层中。例如:1
2
3
4
5
6
7
8
9
10public class BusinessLogic {
private DatabaseAccess databaseAccess;
public void saveData(String data) {
databaseAccess.saveData(data);
}
}这样,我们就不用在业务逻辑层中显式创建 数据库访问接口 的实例了。
总结:
依赖倒转原则可以提高代码的灵活性和可维护性,降低耦合度,提高代码的测试性。
其他示例:
依赖倒转原则可以应用于各种软件设计场景,例如:框架设计: 许多框架都采用了依赖倒转原则,例如 Spring 框架。
测试驱动开发: 依赖倒转原则可以使测试驱动开发更加容易。
里氏代换原则
里氏代换原则:子类型必须能够替换掉它们的夫类型。
假设我们有一个 Animal 类,它定义了一个 eat() 方法:
1 | public class Animal { |
我们还有一个 Dog 类,它继承自 Animal 类,并重写了 eat() 方法:
1 | public class Dog extends Animal { |
现在,我们有一个程序,它使用 Animal 类型的对象:
1 | Animal animal = new Animal(); |
这个程序会输出 “Animal is eating”。
根据里氏代换原则,我们可以将 Animal 类型的对象替换为 Dog 类型的对象,而不会影响程序的执行结果:
1 | Dog dog = new Dog(); |
这个程序也会输出 “Animal is eating”。
重要性
里氏代换原则对于面向对象编程具有重要意义。它可以帮助我们确保程序的健壮性和可扩展性。
优势:当需求有变化,使得需要把“狗”换成“猫”,“猪”等其它动物,程序除了更改实例化的地方其他地方不需要改变,这些动物都具有吃,移动等行为
违反里氏代换原则的示例
以下示例违反了里氏代换原则:
1 | public class Animal { |
在这个例子中,Dog 类的 eat() 方法与 Animal 类的 eat() 方法的签名不同。Dog 类的 eat() 方法需要一个参数,而 Animal 类的 eat() 方法不需要参数。
如果我们将 Animal 类型的对象替换为 Dog 类型的对象,程序就会抛出异常:
1 | Animal animal = new Dog(); |
这个程序会输出 "java.lang.NoSuchMethodException: Animal.eat(java.lang.String)"
遵循里氏代换原则的建议
以下是一些遵循里氏代换原则的建议:
- 子类应该继承父类的所有非抽象方法。
- 子类可以重写父类的非抽象方法,但必须保证重写后的方法的行为与父类的方法的行为一致。
- 子类可以添加新的方法。
总结
里氏代换原则是一条重要的面向对象设计原则。它可以帮助我们确保程序的健壮性和可扩展性。
再来一遍深入理解针对抽象编程!
根据上文你理解了什么是针对抽象编程了吗?
如果还没有理解,那就从上面的代码去思考吧,我们的Service层和Mapper层(或者叫Dao层)都是依赖于Mapper接口去编写这样,应用程序代码(Service层)就无需关心具体的数据库实现细节,只需要关心数据库访问接口。
这里我们举个反例来加强理解
如果直接在Service层中调用Mapper接口的实现类,放弃接口的话,当数据库发生变化时,例如升级到新的版本或更换数据库类型,应用程序代码也需要进行相应的修改。
1 | // 数据库访问类 |
最后通过这两个原则可以凝练出一种面向对象设计的话:
编写时考虑的都是如何针对抽象编程而不是针对细节编程,即程序中所有的依赖关系都是终止于抽象类或者接口